package com.imay.videorecord.encoder.video;

import android.content.Context;
import android.graphics.SurfaceTexture;
import android.opengl.EGLContext;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.imay.videorecord.encoder.MediaAudioEncoder;
import com.imay.videorecord.encoder.MediaEncoder;
import com.imay.videorecord.encoder.MediaMuxerWrapper;
import com.imay.videorecord.encoder.MediaVideoEncoder;
import com.imay.videorecord.encoder.gles.EglCore;
import com.imay.videorecord.filter.base.MagicCameraInputFilter;
import com.imay.videorecord.filter.base.gpuimage.GPUImageFilter;
import com.imay.videorecord.filter.helper.MagicFilterType;
import com.imay.videorecord.utils.MagicFilterTools;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.FloatBuffer;

/**
 * Created by Administrator on 2017/3/12.
 */

public class TextureMovieEncoder implements Runnable {
    private static final String TAG = "TextureMovie";
    private static final boolean VERBOSE = false;

    private static final int MSG_START_RECORDING = 0;
    private static final int MSG_STOP_RECORDING = 1;
    private static final int MSG_FRAME_AVAILABLE = 2;
    private static final int MSG_SET_TEXTURE_ID = 3;
    private static final int MSG_UPDATE_SHARED_CONTEXT = 4;
    private static final int MSG_QUIT = 5;

    // ----- accessed exclusively by encoder thread -----
    private WindowSurface mInputWindowSurface;
    private EglCore mEglCore;
    private MagicCameraInputFilter cameraInputFilter;
    private int mTextureId;
    private int mFrameNum;

    private MediaMuxerWrapper mMuxer;
    private MediaVideoEncoder mVideoEncoder;
    private MediaAudioEncoder mAudioEncoder;

    // ----- accessed by multiple threads -----
    private volatile EncoderHandler mHandler;

    private Object mReadyFence = new Object();      // guards ready/running
    private boolean mReady;
    private boolean mRunning;

    private GPUImageFilter filter; // 滤镜
    private FloatBuffer gLCubeBuffer; // 未知
    private FloatBuffer gLTextureBuffer; // 未知

    private Context mContext;

    private MagicFilterType type = MagicFilterType.NONE;

    private int mPreviewWidth = -1;
    private int mPreviewHeight = -1;
    private boolean releasestatus = false;

    public TextureMovieEncoder(Context context) {
        mContext = context;
    }

    public void setPreviewSize(int previewWidth, int pictureHeight) {
        mPreviewWidth = previewWidth;
        mPreviewHeight = pictureHeight;
    }

    public void setTextureBuffer(FloatBuffer textureBuffer) {
        gLTextureBuffer = textureBuffer;
    }

    public void setCubeBuffer(FloatBuffer cubeBuffer) {
        gLCubeBuffer = cubeBuffer;
    }

    public void setFilter(MagicFilterType type) {
        this.type = type;
    }

    public void release() {
        if (mVideoEncoder != null) {
            mVideoEncoder = null;
        }
        if (mAudioEncoder != null) {
            mAudioEncoder = null;
        }
    }

    /**
     * Encoder configuration.
     * <p>
     * Object is immutable, which means we can safely pass it between threads without
     * explicit synchronization (and don't need to worry about it getting tweaked out from
     * under us).
     * <p>
     * TODO: make frame rate and iframe interval configurable?  Maybe use builder pattern
     * with reasonable defaults for those and bit rate.
     */
    public static class EncoderConfig {
        final String mOutputFilePath;
        final int mWidth;
        final int mHeight;
        final int mBitRate;
        final boolean mIsVolue;
        final EGLContext mEglContext;
        final MediaEncoder.MediaEncoderListener mListener;

        public EncoderConfig(String outputFilePath, int width, int height, int bitRate, boolean isVolue,
                             EGLContext sharedEglContext, MediaEncoder.MediaEncoderListener listener) {
            mOutputFilePath = outputFilePath;
            mWidth = width;
            mHeight = height;
            mBitRate = bitRate;
            mIsVolue = isVolue;
            mEglContext = sharedEglContext;
            mListener = listener;
        }

        @Override
        public String toString() {
            return "EncoderConfig: " + mWidth + "x" + mHeight + " @" + mBitRate +
                    " to '" + mOutputFilePath + "' ctxt=" + mEglContext;
        }
    }

    /**
     * Tells the video recorder to start recording.  (Call from non-encoder thread.)
     * <p>
     * Creates a new thread, which will create an encoder using the provided configuration.
     * <p>
     * Returns after the recorder thread has started and is ready to accept Messages.  The
     * encoder may not yet be fully configured.
     */
    public void startRecording(EncoderConfig config) {
        Log.d(TAG, "Encoder: startRecording()");
        synchronized (mReadyFence) {
            if (mRunning) {
                Log.w(TAG, "Encoder thread already running");
                return;
            }
            mRunning = true;
            new Thread(this, "TextureMovieEncoder").start();
            while (!mReady) {
                try {
                    mReadyFence.wait();
                } catch (InterruptedException ie) {
                    // ignore
                }
            }
        }
        try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
        } catch (IllegalStateException e) {
            Log.e(TAG, "sending message to a Handler on a dead thread");
        }
    }

    /**
     * Tells the video recorder to stop recording.  (Call from non-encoder thread.)
     * <p>
     * Returns immediately; the encoder/muxer may not yet be finished creating the movie.
     * <p>
     * TODO: have the encoder thread invoke a callback on the UI thread just before it shuts down
     * so we can provide reasonable status UI (and let the caller know that movie encoding
     * has completed).
     */
    public void stopRecording() {
        try {
            if (mHandler != null) {
                mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_RECORDING));
                mHandler.sendMessage(mHandler.obtainMessage(MSG_QUIT));
            }
        } catch (IllegalStateException e) {
            Log.e(TAG, "sending message to a Handler on a dead thread");
        }
        // We don't know when these will actually finish (or even start).  We don't want to
        // delay the UI thread though, so we return immediately.
    }

    /**
     * Returns true if recording has been started.
     */
    public boolean isRecording() {
        synchronized (mReadyFence) {
            return mRunning;
        }
    }

    /**
     * Tells the video recorder to refresh its EGL surface.  (Call from non-encoder thread.)
     */
    public void updateSharedContext(EGLContext sharedContext) {
        try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SHARED_CONTEXT, sharedContext));
        } catch (IllegalStateException e) {
            Log.e(TAG, "sending message to a Handler on a dead thread");
        }
    }

    /**
     * Tells the video recorder that a new frame is available.  (Call from non-encoder thread.)
     * <p>
     * This function sends a message and returns immediately.  This isn't sufficient -- we
     * don't want the caller to latch a new frame until we're done with this one -- but we
     * can get away with it so long as the input frame rate is reasonable and the encoder
     * thread doesn't stall.
     * <p>
     * TODO: either block here until the texture has been rendered onto the encoder surface,
     * or have a separate "block if still busy" method that the caller can execute immediately
     * before it calls updateTexImage().  The latter is preferred because we don't want to
     * stall the caller while this thread does work.
     */
    public void frameAvailable(SurfaceTexture st) {
        synchronized (mReadyFence) {
            if (!mReady) {
                return;
            }
        }

        float[] transform = new float[16];      // TODO - avoid alloc every frame
        st.getTransformMatrix(transform);
        long timestamp = st.getTimestamp();
        if (timestamp == 0) {
            // Seeing this after device is toggled off/on with power button.  The
            // first frame back has a zero timestamp.
            //
            // MPEG4Writer thinks this is cause to abort() in native code, so it's very
            // important that we just ignore the frame.
            Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
            return;
        }

        try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
                    (int) (timestamp >> 32), (int) timestamp, transform));
        } catch (IllegalStateException e) {
            Log.e(TAG, "sending message to a Handler on a dead thread");
        }
    }

    /**
     * Tells the video recorder what texture name to use.  This is the external texture that
     * we're receiving camera previews in.  (Call from non-encoder thread.)
     * <p>
     * TODO: do something less clumsy
     */
    public void setTextureId(int id) {
        synchronized (mReadyFence) {
            if (!mReady) {
                return;
            }
        }
        try {
            mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_TEXTURE_ID, id, 0, null));
        } catch (IllegalStateException e) {
            Log.e(TAG, "sending message to a Handler on a dead thread");
        }
    }

    /**
     * Encoder thread entry point.  Establishes Looper/Handler and waits for messages.
     * <p>
     *
     * @see Thread#run()
     */
    @Override
    public void run() {
        // Establish a Looper for this thread, and define a Handler for it.
        Looper.prepare();
        synchronized (mReadyFence) {
            mHandler = new EncoderHandler(this);
            mReady = true;
            mReadyFence.notify();
        }
        Looper.loop();

        Log.d(TAG, "Encoder thread exiting");
        synchronized (mReadyFence) {
            mReady = mRunning = false;
            mHandler = null;
        }
    }


    /**
     * Handles encoder state change requests.  The handler is created on the encoder thread.
     */
    private static class EncoderHandler extends Handler {
        private WeakReference<TextureMovieEncoder> mWeakEncoder;

        public EncoderHandler(TextureMovieEncoder encoder) {
            mWeakEncoder = new WeakReference<TextureMovieEncoder>(encoder);
        }

        @Override  // runs on encoder thread
        public void handleMessage(Message inputMessage) {
            int what = inputMessage.what;
            Object obj = inputMessage.obj;

            TextureMovieEncoder encoder = mWeakEncoder.get();
            if (encoder == null) {
                Log.w(TAG, "EncoderHandler.handleMessage: encoder is null");
                return;
            }

            switch (what) {
                case MSG_START_RECORDING:
                    encoder.handleStartRecording((EncoderConfig) obj);
                    break;
                case MSG_STOP_RECORDING:
                    encoder.handleStopRecording();
                    break;
                case MSG_FRAME_AVAILABLE:
                    long timestamp = (((long) inputMessage.arg1) << 32) |
                            (((long) inputMessage.arg2) & 0xffffffffL);
                    encoder.handleFrameAvailable((float[]) obj, timestamp);
                    break;
                case MSG_SET_TEXTURE_ID:
                    encoder.handleSetTexture(inputMessage.arg1);
                    break;
                case MSG_UPDATE_SHARED_CONTEXT:
                    encoder.handleUpdateSharedContext((EGLContext) inputMessage.obj);
                    break;
                case MSG_QUIT:
                    Looper.myLooper().quit();
                    break;
                default:
                    throw new RuntimeException("Unhandled msg what=" + what);
            }
        }
    }

    /**
     * Starts recording.
     */
    private void handleStartRecording(EncoderConfig config) {
        Log.d(TAG, "handleStartRecording " + config);
        mFrameNum = 0;
        prepareEncoder(config.mEglContext, config.mWidth, config.mHeight, config.mBitRate, config.mIsVolue,
                config.mOutputFilePath, config.mListener);
    }

    /**
     * Handles notification of an available frame.
     * <p>
     * The texture is rendered onto the encoder's input surface, along with a moving
     * box (just because we can).
     * <p>
     *
     * @param transform      The texture transform, from SurfaceTexture.
     * @param timestampNanos The frame's timestamp, from SurfaceTexture.
     */
    private void handleFrameAvailable(float[] transform, long timestampNanos) {
        if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
        if (mVideoEncoder == null || releasestatus) {
            if (VERBOSE) {
                Log.v(TAG, "MediaVideoEncoder not definition Or release");
            }
            return;
        }

        cameraInputFilter.setTextureTransformMatrix(transform);
        if (filter == null) {
            cameraInputFilter.onDraw(mTextureId, gLCubeBuffer, gLTextureBuffer);
        } else {
            //id = cameraInputFilter.onDrawToTexture(mTextureId);
            filter.onDraw(mTextureId, gLCubeBuffer, gLTextureBuffer);
        }

        mInputWindowSurface.setPresentationTime(timestampNanos);
        mInputWindowSurface.swapBuffers();
        if (transform == null) {
            mVideoEncoder.frameAvailableSoon();
        } else {
            mVideoEncoder.frameAvailableSoon(transform);
        }
    }

    /**
     * Handles a request to stop encoding.
     */
    private void handleStopRecording() {
        Log.d(TAG, "handleStopRecording");
        mMuxer.stopRecording();
        releaseEncoder();
    }

    /**
     * Sets the texture name that SurfaceTexture will use when frames are received.
     */
    private void handleSetTexture(int id) {
        //Log.d(TAG, "handleSetTexture " + id);
        mTextureId = id;
    }

    /**
     * Tears down the EGL surface and context we've been using to feed the MediaCodec input
     * surface, and replaces it with a new one that shares with the new context.
     * <p>
     * This is useful if the old context we were sharing with went away (maybe a GLSurfaceView
     * that got torn down) and we need to hook up with the new one.
     */
    private void handleUpdateSharedContext(EGLContext newSharedContext) {
        Log.d(TAG, "handleUpdatedSharedContext " + newSharedContext);

        // Release the EGLSurface and EGLContext.
        mInputWindowSurface.releaseEglSurface();
        cameraInputFilter.destroy();
        cameraInputFilter = null;
        mEglCore.release();
        mEglCore = null;

        // Create a new EGLContext and recreate the window surface.
        mEglCore = new EglCore(newSharedContext, EglCore.FLAG_RECORDABLE);
        mInputWindowSurface.recreate(mEglCore);
        mInputWindowSurface.makeCurrent();

        // Create new programs and such for the new context.
        cameraInputFilter = new MagicCameraInputFilter(mContext);
        cameraInputFilter.init();
        if (type != MagicFilterType.NONE) {
            filter = MagicFilterTools.getInstance(mContext).createFilterForType(mContext, type);
            if (filter != null) {
                filter.init();
                filter.onOutputSizeChanged(mPreviewWidth, mPreviewHeight);
            }
        }
    }

    private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate, boolean isVolue,
                                String outputFilePath, MediaEncoder.MediaEncoderListener listener) {
        try {
            // 清除缓存
            if (mAudioEncoder != null) {
                mAudioEncoder = null;
            }
            if (mVideoEncoder != null) {
                mVideoEncoder = null;
            }
            if (mMuxer != null) {
                mMuxer = null;
            }
            Runtime.getRuntime().gc();

            releasestatus = false;

            mMuxer = new MediaMuxerWrapper(outputFilePath);

            // for video capturing
            mVideoEncoder = new MediaVideoEncoder(mMuxer, listener, width, height);
            // for audio capturing
            if (isVolue) {
                mAudioEncoder = new MediaAudioEncoder(mMuxer, listener);
            }

            mMuxer.prepare();

            mMuxer.startRecording();

            mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
            mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
            mInputWindowSurface.makeCurrent();

            cameraInputFilter = new MagicCameraInputFilter(mContext);
            cameraInputFilter.onInputSizeChanged(mPreviewWidth, mPreviewHeight);
            cameraInputFilter.init();
            if (type != MagicFilterType.NONE) {
                filter = MagicFilterTools.getInstance(mContext).createFilterForType(mContext, type);
                if (filter != null) {
                    filter.init();
                    filter.onOutputSizeChanged(mPreviewWidth, mPreviewHeight);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void releaseEncoder() {
        releasestatus = true;
        if (mInputWindowSurface != null) {
            mInputWindowSurface.release();
            mInputWindowSurface = null;
        }
        if (cameraInputFilter != null) {
            cameraInputFilter.destroy();
            cameraInputFilter = null;
        }
        if (filter != null) {
            filter.destroy();
            filter = null;
        }
        if (mEglCore != null) {
            mEglCore.release();
            mEglCore = null;
        }
    }

}
